%include "boot.inc"

;起始地址为0x0000:0x900
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR


;这里是定义gdt(global descriptor table)一个64位结构在内存上占8字节
GDT_BASE: dd 0x00000000
	  dd 0x00000000

CODE_DESC: dd 0x0000FFFF
	  dd DESC_CODE_HIGH4

DATA_STACK_DESC: dd 0x0000FFFF
	 	 dd DESC_DATA_HIGH4

VIDEO_DESC: dd 0x80000007;limit=(0xbffff-0xb8000)/4k=0x7
 	    dd DESC_VIDEO_HIGH4

;gdt大小限制
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1

;三个gdt描述符的宏定义
SELECTOR_CODE  equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA  equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

;凑整让内存对齐60x8字节
times 60 dq 0

;总共的内存大小放在这 0x0000b00 = 0x00900 +[ (gdt_size [4x8]) + 60 x 8 ] {0x200}
total_mem_bytes dd 0

;gdt的指针 6 字节
gdt_ptr dw GDT_LIMIT
        dd GDT_BASE


;调用内存检查的缓存区 244 字节
ards_buf times 244 db 0
;缓存的数量 2字节   244 + 2 + 6 = 256{0x100}
ards_nr dw 0

;loader_start的地址为0x00000:0x00c00 对于基址 0x0000:0x900 偏移了0x300 所以为0x0000:0x000c00
loader_start:
    ;因为要用到ebx寄存器调用int 0x15先把ebx归0
    ;这里调用三个内存检查功能最终目的是吧内存检查的结果放入total_mem_bytes的地址内
    xor ebx, ebx
    mov edx, 0x534d4150
    mov di, ards_buf
    ;获取内存用0x15中断子功能0e820
    .e820_mem_get_loop:
         mov eax, 0x0000e820
	 mov ecx, 20
	 int 0x15
	 ;如果失败则掉用e801检查内存
	 jc .e820_failed_so_try_e801
         add di, cx
	 inc word [ards_nr]
	 cmp ebx, 0
	 jnz .e820_mem_get_loop
	 mov cx, [ards_nr]
	 mov ebx, ards_buf
	 xor edx, edx
    .find_max_mem_area:
         mov eax, [ebx]
	 add eax, [ebx+8]
	 add ebx, 20
	 cmp edx, eax
	 jge .next_ards
	 mov edx, eax
    .next_ards:
         loop .find_max_mem_area
	 jmp .mem_get_ok
    .e820_failed_so_try_e801:
         mov ax, 0xe801
	 int 0x15
	 jc .e801_failed_so_try_88
	 mov cx, 0x400
	 mul cx
	 shl edx, 16
	 and eax, 0x0000FFFF
	 or edx, eax
	 add edx, 0x100000
	 mul ecx

	 add esi, eax
	 mov edx, esi
	 jmp .mem_get_ok
     .e801_failed_so_try_88:
         mov ah, 0x88
	 int 0x15
	 jc .error_hlt
	 and eax, 0x0000FFFF
	 mov cx, 0x400
	 mul cx
	 shl edx, 16
	 or edx, eax
	 add edx, 0x100000
     .mem_get_ok:
         mov [total_mem_bytes], edx
         

    in al, 0x92
    or al, 0000_00010B
    out 0x92, al

    lgdt [gdt_ptr]

    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

    jmp dword SELECTOR_CODE:p_mode_start

    .error_hlt:
       jmp $


[bits 32]
;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值，是为.create_pte做准备，ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址，
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表，
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(3)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
.create_pte:				     ; 创建Page Table Entry
   mov [ebx+esi*4],edx			     ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性RW和P位为1,US为0
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret


[bits 32]
p_mode_start:
    mov ax, SELECTOR_DATA
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax
    ;将内核从磁盘中写入缓冲区
    mov eax, KERNEL_START_SECTOR
    mov ebx, KERNEL_BIN_BASE_ADDR 
    mov ecx, 200

    call rd_disk_m_32

    call setup_page

    ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
    sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置

    ;将gdt描述符中视频段描述符中的段基址+0xc0000000
    mov ebx, [gdt_ptr + 2]  
    or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
                            ;段描述符的高4字节的最高位是段基址的31~24位

    ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
    add dword [gdt_ptr + 2], 0xc0000000

    add esp, 0xc0000000        ; 将栈指针同样映射到内核地址

    ; 把页目录地址赋给cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax

    ; 打开cr0的pg位(第31位)
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax

    ;在开启分页后,用gdt新的地址重新加载
    lgdt [gdt_ptr]             ; 重新加载

    jmp enter_kernel


enter_kernel:
    call kernel_init
    mov esp, 0xc009f000

    .mov_cursor_to_head:
    	xor bx, bx
        mov dx, 0x03d4
	mov al, 0x0e
	out dx, al
	mov dx, 0x03d5
	mov al, bh
	out dx, al

	mov dx, 0x03d4
	mov al, 0x0f
	out dx, al

	mov dx, 0x03d4
	mov al, 0x0f
	out dx, al
	mov dx, 0x03d5
	mov al, bl
	out dx, al
    	
    jmp KERNEL_ENTRY_POINT

;-------------------------------------------------------------------------------
			   ;功能:读取硬盘n个扇区
rd_disk_m_32:	   
;-------------------------------------------------------------------------------
							 ; eax=LBA扇区号
							 ; ebx=将数据写入的内存地址
							 ; ecx=读入的扇区数
      mov esi,eax	   ; 备份eax
      mov di,cx		   ; 备份扇区数到di
;读写硬盘:
;第1步：设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi	   ;恢复ax

;第2步：将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f	   ;lba第24~27位
      or al,0xe0	   ; 设置7～4位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;第3步：向0x1f7端口写入读命令，0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来

;第4步：检测硬盘状态
  .not_ready:		   ;测试0x1f7端口(status寄存器)的的BSY位
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready	   ;若未准备好,继续等。

;第5步：从0x1f0端口读数据
      mov ax, di	   ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
			   ;在此先用这种方法,在后面内容会用到insw和outsw等

      mov dx, 256	   ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
      mul dx
      mov cx, ax	   
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx		
      mov [ebx], ax
      add ebx, 2
      loop .go_on_read
      ret


kernel_init:
      xor eax, eax
      xor ebx, ebx ; ebx 记录程序头表地址
      xor ecx, ecx ; ecx 记录程序表中program header 数量
      xor edx, edx ; edx 记录 program header 的尺寸 , e_phentsize

      mov dx, [KERNEL_BIN_BASE_ADDR + 42] ;偏移文件42字节是 e_phentsize program_header的尺寸


      mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ;偏移文件28字节代表e_phoff第一个program_header在文件的偏移量

      add ebx, KERNEL_BIN_BASE_ADDR
      ;循环执行遍历程序头
      mov cx, [KERNEL_BIN_BASE_ADDR + 44]  ;偏移文件44字节的是e_phnum, 表示有几个程序头
      .each_segment:
            cmp byte [ebx + 0], PT_NULL
	    ;如果 p_type = PT_NULL 代表此程序头未被使用 
            je .PTNULL

	    push dword [ebx + 16]

	    mov eax, [ebx + 4]

	    add eax, KERNEL_BIN_BASE_ADDR

	    push eax

	    push dword [ebx + 8]

	    call mem_cpy

	    ;清理调用堆栈
	    add esp, 12
      .PTNULL:
            add ebx, edx
      
            loop .each_segment
            ret

;------------------------------------
;输入栈中三个参数(dst, src, size)
;返回空
;------------------------------------
mem_cpy:
      cld
      ;备份ebp
      push ebp
      ;备份esp
      mov ebp, esp
      ;备份ecx
      push ecx

      ;因为压入了两个寄存器的值32bit(4byte)*2之前的参数要从
      mov edi, [ebp + 8]
      mov esi, [ebp + 12]
      mov ecx, [ebp + 16]
      ;重复执行ecx次字符串复制(movsb)从esi复制到edi;注movsb会自动移动这两个指针
      rep movsb


      pop ecx
      pop ebp
      ret
